package com.fdd.op.sdk.internal.token.stroe;

import com.fdd.op.sdk.domain.OAuth2AccessToken;
import com.fdd.op.sdk.internal.token.FddTokenStore;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 本地存储fdd令牌
 * TODO: 缓存机制，建议生产实现FddTokenStore，使用redis缓存中间件来完成
 *
 * @version 2.0.0
 * @author: Fadada
 * @date: 2022/9/9
 */
public class CacheTokenStoreImpl implements FddTokenStore {
    private static final Log log = LogFactory.getLog(CacheTokenStoreImpl.class);
    private static final int DEFAULT_FLUSH_INTERVAL = 1000;
    private final ConcurrentHashMap<String, OAuth2AccessToken> accessTokenStore = new ConcurrentHashMap<>();
    private final ConcurrentHashMap<String, LinkedHashMap<String, String>> authenticationStore = new ConcurrentHashMap<>();
    private final ConcurrentHashMap<String, OAuth2AccessToken> authenticationToAccessTokenStore = new ConcurrentHashMap<>();
    private final DelayQueue<TokenExpiry> expiryQueue = new DelayQueue<TokenExpiry>();
    private final ConcurrentHashMap<String, TokenExpiry> expiryMap = new ConcurrentHashMap<String, TokenExpiry>();
    private int flushInterval = DEFAULT_FLUSH_INTERVAL;
    private AtomicInteger flushCounter = new AtomicInteger(0);

    @Override
    public void storeAccessToken(OAuth2AccessToken token, LinkedHashMap<String, String> map) {
        if (this.flushCounter.incrementAndGet() >= this.flushInterval) {
            flush();
            this.flushCounter.set(0);
        }
        this.accessTokenStore.put(token.getAccessToken(), token);
        this.authenticationStore.put(token.getAccessToken(), map);
        this.authenticationToAccessTokenStore.put(generateKey(map), token);
        if (token.getExpiresIn() != null) {
            long expiration = 0;
            try {
                expiration = Long.parseLong(String.valueOf(token.getExpiresIn()));
            } catch (NumberFormatException e) {
                log.warn("set accessToken expiration is error：{}", e);
            }
            TokenExpiry expiry = new TokenExpiry(token.getAccessToken(), new Date(System.currentTimeMillis() + (expiration * 1000L)));
            // Remove existing expiry for this token if present
            expiryQueue.remove(expiryMap.put(token.getAccessToken(), expiry));
            this.expiryQueue.put(expiry);
        }
    }

//    @Override
//    public OAuth2AccessToken readAccessToken(String tokenValue) {
//        return this.accessTokenStore.get(tokenValue);
//    }
//
//    @Override
//    public void removeAccessToken(OAuth2AccessToken token) {
//        removeAccessToken(token.getAccessToken());
//    }

    @Override
    public OAuth2AccessToken getAccessToken(LinkedHashMap<String, String> map) {
        String key = generateKey(map);
        return authenticationToAccessTokenStore.get(key);
    }

    protected void removeAccessToken(String tokenValue) {
        this.accessTokenStore.remove(tokenValue);
        LinkedHashMap<String, String> map = this.authenticationStore.get(tokenValue);
        if (map != null) {
            this.authenticationToAccessTokenStore.remove(generateKey(map));
        }
        this.authenticationStore.remove(tokenValue);
    }

    protected void flush() {
        TokenExpiry expiry = expiryQueue.poll();
        while (expiry != null) {
            removeAccessToken(expiry.getValue());
            expiry = expiryQueue.poll();
        }
    }

    /**
     * 加密key
     *
     * @throws
     * @Date: 2022/9/9
     * @Param: [values]
     * @Return: java.lang.String
     */
    protected String generateKey(LinkedHashMap<String, String> values) {
        MessageDigest digest;
        try {
            digest = MessageDigest.getInstance("MD5");
            byte[] bytes = digest.digest(values.toString().getBytes("UTF-8"));
            return String.format("%032x", new BigInteger(1, bytes));
        } catch (NoSuchAlgorithmException nsae) {
            throw new IllegalStateException("MD5 algorithm not available.  Fatal (should be in the JDK).", nsae);
        } catch (UnsupportedEncodingException uee) {
            throw new IllegalStateException("UTF-8 encoding not available.  Fatal (should be in the JDK).", uee);
        }
    }

    private static class TokenExpiry implements Delayed {

        private final long expiry;

        private final String value;

        public TokenExpiry(String value, Date date) {
            this.value = value;
            this.expiry = date.getTime();
        }

        @Override
        public int compareTo(Delayed other) {
            if (this == other) {
                return 0;
            }
            long diff = getDelay(TimeUnit.MILLISECONDS) - other.getDelay(TimeUnit.MILLISECONDS);
            return (diff == 0 ? 0 : ((diff < 0) ? -1 : 1));
        }

        @Override
        public long getDelay(TimeUnit unit) {
            return expiry - System.currentTimeMillis();
        }

        public String getValue() {
            return value;
        }
    }
}
